// Copyright 2019 Google LLC
//
// Licensed under the Apache License, Version 2.0 (the "License");
// you may not use this file except in compliance with the License.
// You may obtain a copy of the License at
//
//     https://www.apache.org/licenses/LICENSE-2.0
//
// Unless required by applicable law or agreed to in writing, software
// distributed under the License is distributed on an "AS IS" BASIS,
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
// See the License for the specific language governing permissions and
// limitations under the License.

package main

import (
	"context"
	"errors"
	"log"
	"time"

	"google.golang.org/api/iterator"

	"cloud.google.com/go/firestore"
)

func addDocAsMap(ctx context.Context, client *firestore.Client) error {
	// [START firestore_data_set_from_map]
	_, err := client.Collection("cities").Doc("LA").Set(ctx, map[string]interface{}{
		"name":    "Los Angeles",
		"state":   "CA",
		"country": "USA",
	})
	if err != nil {
		// Handle any errors in an appropriate way, such as returning them.
		log.Printf("An error has occurred: %s", err)
	}
	// [END firestore_data_set_from_map]
	return err
}

func addDocDataTypes(ctx context.Context, client *firestore.Client) error {
	// [START firestore_data_set_from_map_nested]
	doc := make(map[string]interface{})
	doc["stringExample"] = "Hello world!"
	doc["booleanExample"] = true
	doc["numberExample"] = 3.14159265
	doc["dateExample"] = time.Now()
	doc["arrayExample"] = []interface{}{5, true, "hello"}
	doc["nullExample"] = nil
	doc["objectExample"] = map[string]interface{}{
		"a": 5,
		"b": true,
	}

	_, err := client.Collection("data").Doc("one").Set(ctx, doc)
	if err != nil {
		// Handle any errors in an appropriate way, such as returning them.
		log.Printf("An error has occurred: %s", err)
	}
	// [END firestore_data_set_from_map_nested]
	return err
}

func addDocWithID(ctx context.Context, client *firestore.Client) error {
	var data = make(map[string]interface{})
	// [START firestore_data_set_id_specified]
	_, err := client.Collection("cities").Doc("new-city-id").Set(ctx, data)
	if err != nil {
		// Handle any errors in an appropriate way, such as returning them.
		log.Printf("An error has occurred: %s", err)
	}
	// [END firestore_data_set_id_specified]
	return err
}

func addDocWithoutID(ctx context.Context, client *firestore.Client) error {
	// [START firestore_data_set_id_random_collection]
	_, _, err := client.Collection("cities").Add(ctx, map[string]interface{}{
		"name":    "Tokyo",
		"country": "Japan",
	})
	if err != nil {
		// Handle any errors in an appropriate way, such as returning them.
		log.Printf("An error has occurred: %s", err)
	}
	// [END firestore_data_set_id_random_collection]
	return err
}

func addDocAsEntity(ctx context.Context, client *firestore.Client) error {
	// [START firestore_data_set_from_custom_type]
	city := City{
		Name:    "Los Angeles",
		Country: "USA",
	}
	_, err := client.Collection("cities").Doc("LA").Set(ctx, city)
	if err != nil {
		// Handle any errors in an appropriate way, such as returning them.
		log.Printf("An error has occurred: %s", err)
	}
	// [END firestore_data_set_from_custom_type]
	return err
}

func addDocAfterAutoGeneratedID(ctx context.Context, client *firestore.Client) error {
	data := City{
		Name:    "Sydney",
		Country: "Australia",
	}

	// [START firestore_data_set_id_random_document_ref]
	ref := client.Collection("cities").NewDoc()

	// later...
	_, err := ref.Set(ctx, data)
	if err != nil {
		// Handle any errors in an appropriate way, such as returning them.
		log.Printf("An error has occurred: %s", err)
	}
	// [END firestore_data_set_id_random_document_ref]
	return err
}

func updateDoc(ctx context.Context, client *firestore.Client) error {
	_, err := client.Collection("cities").Doc("DC").Set(ctx, map[string]interface{}{
		"name":    "District of Columbia",
		"country": "USA",
	})
	if err != nil {
		log.Printf("adding city DC: %s", err)
	}
	// [START firestore_data_set_field]
	_, err = client.Collection("cities").Doc("DC").Update(ctx, []firestore.Update{
		{
			Path:  "capital",
			Value: true,
		},
	})
	if err != nil {
		// Handle any errors in an appropriate way, such as returning them.
		log.Printf("An error has occurred: %s", err)
	}
	// [END firestore_data_set_field]
	return err
}

func updateDocCreateIfMissing(ctx context.Context, client *firestore.Client) error {
	// [START firestore_data_set_doc_upsert]
	_, err := client.Collection("cities").Doc("BJ").Set(ctx, map[string]interface{}{
		"capital": true,
	}, firestore.MergeAll)

	if err != nil {
		// Handle any errors in an appropriate way, such as returning them.
		log.Printf("An error has occurred: %s", err)
	}
	// [END firestore_data_set_doc_upsert]
	return err
}

func updateDocMultiple(ctx context.Context, client *firestore.Client) error {
	_, err := client.Collection("cities").Doc("Delhi").Set(ctx, City{Name: "Delhi"})
	if err != nil {
		return err
	}

	// [START fs_update_multiple_fields]
	_, err = client.Collection("cities").Doc("Delhi").Set(ctx, map[string]interface{}{
		"capital":           true,
		"country":           "India",
		"population":        16787941,
		"areaInSquareMiles": 573.0,
	}, firestore.MergeAll)
	if err != nil {
		// Handle any errors in an appropriate way, such as returning them.
		log.Printf("An error has occurred: %s", err)
	}
	// [END fs_update_multiple_fields]
	return err
}

func updateDocNested(ctx context.Context, client *firestore.Client) error {
	// [START firestore_data_set_nested_fields]
	initialData := map[string]interface{}{
		"name": "Frank",
		"age":  12,
		"favorites": map[string]interface{}{
			"food":    "Pizza",
			"color":   "Blue",
			"subject": "recess",
		},
	}

	// [START_EXCLUDE]

	if _, err := client.Collection("users").Doc("frank").Set(ctx, initialData); err != nil {
		return err
	}
	// [END_EXCLUDE]

	_, err := client.Collection("users").Doc("frank").Set(ctx, map[string]interface{}{
		"age": 13,
		"favorites": map[string]interface{}{
			"color": "Red",
		},
	}, firestore.MergeAll)
	if err != nil {
		// Handle any errors in an appropriate way, such as returning them.
		log.Printf("An error has occurred: %s", err)
	}
	// [END firestore_data_set_nested_fields]
	return err
}

func updateDocServerTimestamp(ctx context.Context, client *firestore.Client) error {
	_, preErr := client.Collection("objects").Doc("some-id").Set(ctx, map[string]interface{}{
		"timestamp": 0,
	})
	if preErr != nil {
		return preErr
	}

	// [START firestore_data_set_server_timestamp]
	_, err := client.Collection("objects").Doc("some-id").Set(ctx, map[string]interface{}{
		"timestamp": firestore.ServerTimestamp,
	}, firestore.MergeAll)
	if err != nil {
		// Handle any errors in an appropriate way, such as returning them.
		log.Printf("An error has occurred: %s", err)
	}
	// [END firestore_data_set_server_timestamp]
	return err
}

func deleteDoc(ctx context.Context, client *firestore.Client) error {
	// [START firestore_data_delete_doc]
	_, err := client.Collection("cities").Doc("DC").Delete(ctx)
	if err != nil {
		// Handle any errors in an appropriate way, such as returning them.
		log.Printf("An error has occurred: %s", err)
	}
	// [END firestore_data_delete_doc]
	return err
}

func deleteField(ctx context.Context, client *firestore.Client) error {
	// [START firestore_data_delete_field]
	_, err := client.Collection("cities").Doc("BJ").Update(ctx, []firestore.Update{
		{
			Path:  "capital",
			Value: firestore.Delete,
		},
	})
	if err != nil {
		// Handle any errors in an appropriate way, such as returning them.
		log.Printf("An error has occurred: %s", err)
	}
	// [END firestore_data_delete_field]

	// Use Set once this feature is implemented:
	// https://github.com/GoogleCloudPlatform/google-cloud-go/issues/832
	// Set(ctx, map[string]interface{}{
	//	"capital": firestore.Delete,
	//})
	return err
}

// [START firestore_data_delete_collection]
func deleteCollection(ctx context.Context, client *firestore.Client,
	ref *firestore.CollectionRef, batchSize int) error {

	for {
		// Get a batch of documents
		iter := ref.Limit(batchSize).Documents(ctx)
		numDeleted := 0

		// Iterate through the documents, adding
		// a delete operation for each one to a
		// WriteBatch.
		batch := client.Batch()
		for {
			doc, err := iter.Next()
			if err == iterator.Done {
				break
			}
			if err != nil {
				return err
			}

			batch.Delete(doc.Ref)
			numDeleted++
		}

		// If there are no documents to delete,
		// the process is over.
		if numDeleted == 0 {
			return nil
		}

		_, err := batch.Commit(ctx)
		if err != nil {
			return err
		}
	}
}

// [END firestore_data_delete_collection]

func runSimpleTransaction(ctx context.Context, client *firestore.Client) error {
	_, preErr := client.Collection("cities").Doc("SF").Set(ctx, map[string]interface{}{
		"population": 860000,
	})

	if preErr != nil {
		return preErr
	}

	// [START firestore_transaction_document_update]
	ref := client.Collection("cities").Doc("SF")
	err := client.RunTransaction(ctx, func(ctx context.Context, tx *firestore.Transaction) error {
		doc, err := tx.Get(ref) // tx.Get, NOT ref.Get!
		if err != nil {
			return err
		}
		pop, err := doc.DataAt("population")
		if err != nil {
			return err
		}
		return tx.Set(ref, map[string]interface{}{
			"population": pop.(int64) + 1,
		}, firestore.MergeAll)
	})
	if err != nil {
		// Handle any errors appropriately in this section.
		log.Printf("An error has occurred: %s", err)
	}
	// [END firestore_transaction_document_update]
	return err
}

func infoTransaction(ctx context.Context, client *firestore.Client) error {
	// [START firestore_transaction_document_update_conditional]
	ref := client.Collection("cities").Doc("SF")
	err := client.RunTransaction(ctx, func(ctx context.Context, tx *firestore.Transaction) error {
		doc, err := tx.Get(ref)
		if err != nil {
			return err
		}
		pop, err := doc.DataAt("population")
		if err != nil {
			return err
		}
		newpop := pop.(int64) + 1
		if newpop <= 1000000 {
			return tx.Set(ref, map[string]interface{}{
				"population": pop.(int64) + 1,
			}, firestore.MergeAll)
		}
		return errors.New("population is too big")
	})
	if err != nil {
		// Handle any errors in an appropriate way, such as returning them.
		log.Printf("An error has occurred: %s", err)
	}
	// [END firestore_transaction_document_update_conditional]
	return err
}

func batchWrite(ctx context.Context, client *firestore.Client) error {
	// [START firestore_data_batch_writes]
	// Get a new write batch.
	batch := client.Batch()

	// Set the value of "NYC".
	nycRef := client.Collection("cities").Doc("NYC")
	batch.Set(nycRef, map[string]interface{}{
		"name": "New York City",
	})

	// Update the population of "SF".
	sfRef := client.Collection("cities").Doc("SF")
	batch.Set(sfRef, map[string]interface{}{
		"population": 1000000,
	}, firestore.MergeAll)

	// Delete the city "LA".
	laRef := client.Collection("cities").Doc("LA")
	batch.Delete(laRef)

	// Commit the batch.
	_, err := batch.Commit(ctx)
	if err != nil {
		// Handle any errors in an appropriate way, such as returning them.
		log.Printf("An error has occurred: %s", err)
	}
	// [END firestore_data_batch_writes]
	return err
}
